Skip to content

fix: prevent graph chart infinite loading loop on fullscreen expand (#313) - #322

Merged
alfredo1996 merged 3 commits into
release/1.0from
fix/313-graph-chart-fullscreen-loop
Apr 2, 2026
Merged

fix: prevent graph chart infinite loading loop on fullscreen expand (#313)#322
alfredo1996 merged 3 commits into
release/1.0from
fix/313-graph-chart-fullscreen-loop

Conversation

@alfredo1996

@alfredo1996 alfredo1996 commented Apr 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Added 800ms safety timeout in graph-chart.tsx that forces layoutReady=true if NVL onLayoutDone never fires (happens when layout starts during dialog animation with degenerate container size)
  • Added widgetIdSuffix prop to CardContainer to prevent Zustand store conflicts between normal and fullscreen graph instances
  • Fullscreen CardContainer now uses widgetId--fullscreen as store key

Closes #313

Test plan

  • Open a dashboard with graph charts, click fullscreen — graph should render without infinite spinner
  • Normal (non-fullscreen) graph rendering should be unaffected
  • Multiple graph widgets on same page should work independently
  • App tests pass (1676/1676)
  • Component tests pass (1206/1206)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added widgetIdSuffix prop to CardContainer for widget identification flexibility
    • DashboardContainer now applies widget identification suffix in fullscreen mode
  • Bug Fixes

    • GraphChart now includes an 800ms safety timeout to dismiss the loading overlay if the layout callback does not complete
  • Tests

    • Improved error card assertion reliability in connection tests
    • Added CardContainer property validation test suite
    • Added GraphChart loading overlay timeout coverage tests

…313)

Two root causes addressed:

1. NVL layout timeout — When the graph chart mounts inside a CSS-animated
   dialog (fullscreen expand), the container starts at ~0 size during the
   zoom-in-95 animation. NVL's force layout can fail to converge in this
   state and never fire onLayoutDone, leaving the loading spinner visible
   indefinitely. Added a safety timeout (800ms) that forces layoutReady
   if onLayoutDone hasn't fired, then calls fitGraph to re-center.

2. Zustand store conflict — The fullscreen dialog renders a second
   CardContainer for the same widget, creating two GraphExplorationWrapper
   instances that both read/write the same graph widget store slot. Added
   a widgetIdSuffix prop so the fullscreen instance uses a distinct store
   key (widget.id--fullscreen), preventing re-render cascades between the
   normal and fullscreen views.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 2, 2026

Copy link
Copy Markdown

Walkthrough

The PR adds an 800ms safety timeout to GraphChart's loading state to prevent infinite loading loops when the NVL library's onLayoutDone callback doesn't fire, and introduces a widgetIdSuffix prop to differentiate fullscreen widget instances from their normal counterparts in metadata passed to ChartRenderer. This prevents state conflicts during fullscreen expansion.

Changes

Cohort / File(s) Summary
Graph Chart Safety Timeout
component/src/charts/graph-chart.tsx, component/src/charts/__tests__/graph-chart.test.tsx
Adds an 800ms fallback timeout that forces layout readiness and calls fitGraph when NVL's onLayoutDone callback may not fire. Tests validate overlay dismissal, cleanup on unmount, and non-interference when callback fires normally.
Widget ID Suffix for Fullscreen
app/src/components/card-container.tsx, app/src/components/dashboard-container.tsx, app/src/components/__tests__/card-container.test.tsx
Introduces widgetIdSuffix prop to CardContainer to compute effective widget IDs, propagates to ChartRenderer metadata across all rendering paths, and passes widgetIdSuffix="fullscreen" in DashboardContainer's fullscreen dialog. Tests validate default, suffix appending, and propagation behavior.
E2E Test Refinement
app/e2e/connections.spec.ts
Simplifies alert locator strategy from text-filtered first match to role-based last match, reducing flakiness in error card expansion assertions.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • #253: Modifies graph-chart.tsx for relationship-click event handling and refs.
  • #296: Modifies card-container.tsx for click-handling refactoring.
  • #91: Modifies dashboard-container.tsx for component rendering and props updates.

Suggested labels

enhancement, pkg:app, area:widgets

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: preventing an infinite loading loop in graph charts during fullscreen expand, with a reference to the issue number.
Linked Issues check ✅ Passed All coding objectives from issue #313 are met: safety timeout prevents infinite loading when onLayoutDone doesn't fire, widgetIdSuffix prevents Zustand store conflicts, and fullscreen uses distinct widget identifiers.
Out of Scope Changes check ✅ Passed Changes are focused and aligned with PR objectives: graph-chart timeout implementation, widgetIdSuffix propagation, E2E test stability improvement, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/313-graph-chart-fullscreen-loop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@alfredo1996
alfredo1996 changed the base branch from dev to release/1.0 April 2, 2026 13:55
alfredorubin96 and others added 2 commits April 2, 2026 16:05
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…gate

Cover the 800ms safety timeout in GraphChart that prevents infinite loading
when onLayoutDone never fires, and the widgetIdSuffix prop in CardContainer
that prevents graph store conflicts between normal and fullscreen views.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/components/dashboard-container.tsx (1)

354-362: ⚠️ Potential issue | 🟠 Major

Scope fullscreen suffix to graph widgets only.

At Line 361, widgetIdSuffix="fullscreen" is passed for every widget type. For parameter widgets, that can change source IDs to ...--fullscreen, which won’t match dashboard data-widget-id values and can break “jump to source widget” behavior.

Suggested fix
                 {fullscreenReady ? (
                   <CardContainer
                     key={`${fullscreenWidget.id}-fullscreen`}
                     widget={fullscreenWidget}
                     refetchInterval={refetchInterval}
                     onNavigateToPage={onNavigateToPage}
                     parameterSourceMap={parameterSourceMap}
                     autoFit
-                    widgetIdSuffix="fullscreen"
+                    widgetIdSuffix={
+                      fullscreenWidget.chartType === "graph"
+                        ? "fullscreen"
+                        : undefined
+                    }
                   />
                 ) : (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/dashboard-container.tsx` around lines 354 - 362, The
CardContainer is receiving widgetIdSuffix="fullscreen" for every
fullscreenWidget which appends --fullscreen to parameter widget IDs and breaks
jump-to-source matching; update the render so widgetIdSuffix is only passed for
graph widgets (e.g., check fullscreenWidget.type or a helper like
isGraphWidget(fullscreenWidget)) and omit widgetIdSuffix for non-graph/parameter
widgets, keeping the rest of the props (refetchInterval, onNavigateToPage,
parameterSourceMap, autoFit) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/e2e/connections.spec.ts`:
- Around line 190-191: The alert locator is too broad—don't use
page.locator(...).last() because it can pick up alerts from other cards; instead
scope the alert to the clicked card by using the existing card locator (e.g.,
replace page.locator('[role="alert"]').last() with
card.locator('[role="alert"]') or card.locator('role=alert')) and keep the await
expect(...).toBeVisible({ timeout: 5_000 }) call to assert visibility.

---

Outside diff comments:
In `@app/src/components/dashboard-container.tsx`:
- Around line 354-362: The CardContainer is receiving
widgetIdSuffix="fullscreen" for every fullscreenWidget which appends
--fullscreen to parameter widget IDs and breaks jump-to-source matching; update
the render so widgetIdSuffix is only passed for graph widgets (e.g., check
fullscreenWidget.type or a helper like isGraphWidget(fullscreenWidget)) and omit
widgetIdSuffix for non-graph/parameter widgets, keeping the rest of the props
(refetchInterval, onNavigateToPage, parameterSourceMap, autoFit) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e8d44f55-41ba-46bf-b604-16b717e654a3

📥 Commits

Reviewing files that changed from the base of the PR and between f31188f and 99b190a.

📒 Files selected for processing (6)
  • app/e2e/connections.spec.ts
  • app/src/components/__tests__/card-container.test.tsx
  • app/src/components/card-container.tsx
  • app/src/components/dashboard-container.tsx
  • component/src/charts/__tests__/graph-chart.test.tsx
  • component/src/charts/graph-chart.tsx

Comment on lines +190 to +191
const expandedAlert = page.locator('[role="alert"]').last();
await expect(expandedAlert).toBeVisible({ timeout: 5_000 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Playwright, does locator.last() resolve dynamically on each assertion/action, and can it point to a different element after DOM updates?

💡 Result:

Yes, in Playwright, locator.last resolves dynamically on each assertion or action. It re-evaluates the locator query against the current DOM state every time it is used, so it can point to a different element if the DOM updates (such as elements being added, removed, or reordered) between uses. This behavior is core to how all Playwright locators work: they store a query rather than a fixed element reference, ensuring resilience to DOM changes. Official documentation states: "Every time a locator is used for an action, an up-to-date DOM element is located in the page." For example: const locator = page.locator('ul li'); await locator.last.click; // Finds current last

  • await page.waitForTimeout(1000); // DOM changes await locator.last.click; // Finds new last
  • , potentially different The .last method creates a new locator pointing to the last matching element from the parent locator's current matches at resolution time. Methods like .first, .last, and .nth are used to opt out of strictness when multiple elements match, but they inherit the dynamic re-querying. Note that .all does not wait and can be flaky with dynamic lists, but actions/assertions on .last include auto-waiting and re-resolution.

    Citations:


    🏁 Script executed:

    cat -n app/e2e/connections.spec.ts | sed -n '185,200p'

    Repository: alfredo1996/neoboard

    Length of output: 699


    🏁 Script executed:

    cat -n app/e2e/connections.spec.ts | sed -n '170,210p'

    Repository: alfredo1996/neoboard

    Length of output: 2072


  • Scope the alert locator to the clicked card; global .last() is brittle in e2e tests.

    On Line 190, page.locator('[role="alert"]').last() re-evaluates dynamically. In e2e tests where multiple cards exist, this can target the wrong alert after DOM changes or when other test cases add alerts to the page. Since card is already scoped on line 180, use it to isolate the alert:

    Suggested fix
    -    const expandedAlert = page.locator('[role="alert"]').last();
    +    const expandedAlert = card
    +      .locator('[role="alert"]')
    +      .filter({ hasText: /refused|ECONNREFUSED|failed|error/i })
    +      .first();
         await expect(expandedAlert).toBeVisible({ timeout: 5_000 });
    
         // Click again to collapse
         await card.click();
    -    await expect(expandedAlert).not.toBeVisible();
    +    await expect(expandedAlert).not.toBeVisible({ timeout: 5_000 });
    🤖 Prompt for AI Agents
    Verify each finding against the current code and only fix it if needed.
    
    In `@app/e2e/connections.spec.ts` around lines 190 - 191, The alert locator is too
    broad—don't use page.locator(...).last() because it can pick up alerts from
    other cards; instead scope the alert to the clicked card by using the existing
    card locator (e.g., replace page.locator('[role="alert"]').last() with
    card.locator('[role="alert"]') or card.locator('role=alert')) and keep the await
    expect(...).toBeVisible({ timeout: 5_000 }) call to assert visibility.
    

    @sonarqubecloud

    sonarqubecloud Bot commented Apr 2, 2026

    Copy link
    Copy Markdown

    @alfredo1996
    alfredo1996 merged commit 7935997 into release/1.0 Apr 2, 2026
    11 of 13 checks passed
    @alfredo1996
    alfredo1996 deleted the fix/313-graph-chart-fullscreen-loop branch April 7, 2026 11:46
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    None yet

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    bug: graph chart expand/fullscreen causes infinite loading loop

    2 participants